Previous Page TOC Next Page


37 — Creating Your Own Screen Savers

by Bill Hatfield

Screen savers have taken on a life of their own in the computer industry. These whimsical distractions originally were created for a serious purpose: to stop phosphor burn-in of text or graphics on a monitor. Have you ever seen a monitor on a computer or (very often) an ATM that looks as though you can see a shadow of its title screen or main menu, even when something else is being displayed? This shadow is the result of displaying that same screen for hours on end and permanently burning its image into the monitor.

Obviously, this is not a good thing. So someone, somewhere, decided to blank the screen after a while to keep this from happening. But this made the users think the computer had shut itself off. So the developer created an animated display to make it clear that the computer was, in fact, still alive. And this was the beginning of something big.

Although today, modern color monitors are not really at risk of anything like phosphor burn-in, screen-savers have become a big business. Developers always seem to have a surplus of creativity and are just looking for nooks and crannies where they can express it. And they have exploited this blank computer screen to the hilt for all sorts of complex geometric designs, 3D rendering, animations, video clips, and cartoons. And users have bought them up. They enjoy watching as much as developers enjoy creating. In the computer industry, screen savers have come to express the triumph of creativity over mundane productivity.

Making Your Own Screen Saver

Isn't it difficult to create your own screen saver? Don't you have to muck about with difficult system coding to integrate with the Windows environment? Actually, no. Although there is some complexity when creating screen savers in C or C++, Visual Basic makes it very easy.

In this chapter, I will walk you through the process of creating a screen saver of your own. Once you have the basic structure down, your imagination is your only limit in creating exciting new screen savers of your own. Create them for yourself, your co-workers, or share them with the rest of the world for fun or for profit.

Laying the Foundation

As you may have noticed, Windows screen savers have the extension SCR, and they are placed in the Windows subdirectory. They are recognized and displayed as options in the Display Properties dialog box on the Screen Saver tab.

These SCR files are nothing mysterious. In fact, they are nothing more than EXE files that have been renamed! And Windows handles calling these disguised EXEs whenever they should be invoked. So you don't even have to worry about when the screen saver should start up. All of that is handled for you by Windows.

So what do you have to worry about? Well, to create a very simple screen saver, just two things:

Taking the First Steps

Your first step is to start a new project that has one form and one module. In the module, create a procedure called Main that receives no arguments, and set it to be the first thing called on startup. You do this by choosing Tools | Options and then working in the Project tab of the Options dialog box, in the Startup Form drop-down list (see Figure 37.1).


Figure 37.1. Setting the Main procedure to be the first thing called.

You want to execute a procedure first thing, because you want to do some checking before you go to the trouble of loading a form. The code for Main is shown in Listing 37.1.

Sub Main()

If App.PrevInstance Then End

If InStr(Command, "/s") > 0 Then

    CursorOff

    frmScreenSaver.Show

ElseIf InStr(Command, "/c") > 0 Then

    MsgBox "No setup necessary for this screen saver", _

        vbOKOnly + vbInformation, "Boxes Screen Saver"

End If

End Sub

First off, you want to check to see whether there is another instance of the program running. If there is, get out.

Otherwise, you call the Command function to find out whether any command-line arguments were passed when the screen saver was called. Why would Windows call the screen saver with command-line arguments? To let you know what it wants you to do. Windows might want you to simply display the screen saver. If this is the case, it sends you an /s. But when the user clicks the Settings button in the Display Properties dialog box (see Figure 37.2), Windows calls your screen saver with a /c. Because I want to focus on creating a simple screen saver to start, a message box explaining that there is no setup necessary is all you need for that case.


Figure 37.2. After the user clicks the Settings button, the screen saver is called with the /c command-line argument.

Hiding the Mouse Pointer

If Windows does request the actual screen saver (with /s in the command line), then you want to turn off the cursor (the mouse pointer) and display the screen saver. You don't want the ugly mouse pointer obscuring the user's view of your beautiful creation, do you? So you need to create a new procedure called CursorOff. There isn't a Visual Basic command that enables you to do this, so you will have to resort to the ShowCursor() Win32 API function. Put the following code in your module's Declarations section:

Declare Function ShowCursor Lib "user32" (ByVal bShow As Long) As Long

ShowCursor accepts one value. If the value is False (0), it hides the cursor, and if it is True (non-zero), it shows the cursor. Simple, yes? Well, not quite so simple.

When Windows first starts up, it creates a cursor-display counter and sets it to 0. The mouse pointer is displayed when the counter is 0 or greater. If ShowCursor(False) is called, the counter is decremented, putting it at —1. This causes the cursor to disappear, because any counter value below 0 hides the cursor. But then, suppose that another task in Windows makes a call to ShowCursor(False). The counter is decremented again to —2. The cursor was invisible before and remains invisible. Then that task finishes whatever it was doing and does a ShowCursor(True). This increments the display counter back up to —1. But because this is still less than zero, it still is not displayed. Finally, the first task that made the call to ShowCursor(False) finishes up and does a ShowCursor(True). The counter is incremented to 0, and finally the mouse pointer appears again. Make sense? Windows wants to be sure that all the tasks work well together. If task 1 turns off the cursor and task 2 turns it off and back on, task 1 will expect that it is still off until it turns it back on.

It works the same way in the other direction. Several calls to ShowCursor(True) cause the cursor-display counter to increment to higher and higher positive values. Only an equal number of ShowCursor(False) calls actually cause the cursor to disappear.

The catch is that you really, really want it to disappear, regardless of what the display counter is. But, at the same time, you don't want to disturb the counter for other applications. Listing 37.2 shows the CursorOff procedure that will do what you want.

Public Sub CursorOff()

Dim CurrentCursorDepth As Integer

CurrentCursorDepth = ShowCursor(False)

giCursorDepth = CurrentCursorDepth + 1 'Restore original value

Do While CurrentCursorDepth > —1

    CurrentCursorDepth = ShowCursor(False)

Loop

End Sub

The first thing you do is call ShowCursor(False) and receive its return code into CurrentCursorDepth. ShowCursor() returns the value of the display counter after it is changed by the call, so you increment the value and put the result in giCursorDepth to return it to the original cursor-display value before you touched it. That way, you can restore it back to this original value later, when CursorOn is called. giCursorDepth is a global defined in the Declarations section of the module:

Global giCursorDepth As Integer

Next, you loop while the current cursor depth is greater than —1. In other words, regardless of where it is now, get it less than 0. If it already is less than zero, this top-tested loop does not execute.

While we're on this topic, let's look at the CursorOn procedure (see Listing 37.3).

Public Sub CursorOn()

Dim CurrentCursorDepth As Integer

CurrentCursorDepth = ShowCursor(True)

Do While CurrentCursorDepth < giCursorDepth

    CurrentCursorDepth = ShowCursor(True)

Loop

End Sub

First, you increment the counter with the ShowCursor(True) and capture the new CurrentCursorDepth. Then the loop tests to see whether the display counter is back where it belongs. If it isn't, it does ShowCursor(True) statements until it is. So you turned the cursor off and back on again without disturbing the cursor-display counter.

Creating the Screen Saver Form

Look back at Listing 37.1. Before we jumped into our discussion on CursorOn and CursorOff, we were looking at the Main procedure. If Windows passes an /s command line argument, that's your cue to display the actual screen saver. After the cursor is turned off, the code displays frmScreenSaver.

I told you to start off this project by creating a module and a form. So far, we've been talking about the module. Now let's look at the form.

The first thing you will want to do is to name your form frmScreenSaver, because that is what you are showing in the Main procedure. You also will want to change some of the following properties.

Property


Value


BackColor

Black

BorderStyle

None

ControlBox

False

MaxButton

False

MinButton

False

ScaleMode

Pixel

WindowState

Maximized

Next, drop a timer on the form. Leave the default name Timer1, but change the interval to 1,000.

There is no Titlebar True/False property, but changing the BorderStyle to None and the ControlBox, MaxButton, and MinButton to False effectively gets rid of the title bar for you. So, once the window is maximized, it covers everything else on-screen and provides a large blank slate to do your magic.

Finally, you might want to create an icon for your form. If you associate it with the form's Icon property, you'll be able to choose it as the application's icon when you generate an EXE.

When you are done setting up your window, it should look like Figure 37.3.


Figure 37.3. The frmScreenSaver form.

Setting Up the Timer Event (The Fun Part)

Just so there won't be any doubt, we have now arrived at the fun part. This is where you loosen the tie and let your creative juices flow. The Timer event gets executed once a second. You can make that much quicker, if you like, by adjusting the Interval property. The number there is in milliseconds, but you probably aren't going to get much code to execute in a millisecond, so be reasonable in how low you set this property.

Regardless of the interval value, you now have a place to write code that will happen every so often. What do you want to do? Just for the sake of example, I created a very simple graphics routine. My Timer event code is in Listing 37.4.

Private Sub Timer1_Timer()

Dim X1, Y1, X2, Y2 As Integer

Randomize

X1 = Rnd * (ScaleWidth — 1) + 1

Y1 = Rnd * (ScaleHeight — 1) + 1

X2 = Rnd * (ScaleWidth — 1) + 1

Y2 = Rnd * (ScaleHeight — 1) + 1

ForeColor = RGB(Rnd * 255 + 1, Rnd * 255 + 1, Rnd * 255 + 1)

Line (X1, Y1)—(X2, Y2), , BF

End Sub

After declaring some variables and seeding the random number generator, you create a couple of random coordinates in your window's workspace and set the ForeColor property to a random color. Then, using the Line command's BF option, you draw a filled-in box with the upper- left and lower-right corners defined by the random coordinates. Nothing too amazing here but, like I said, we'll keep it basic this time around and once you get the hang of it, you can jazz it up all you want.

Waking Up

The Timer event continues to fire indefinitely unless we stop it somehow. And how are most screen savers stopped? By a mouse click or a keystroke. Listings 37.5 and 37.6 show you what should be in your MouseDown and KeyDown events.

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, _

  X As Single, Y As Single)

EndScreenSaver

End Sub
Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)

EndScreenSaver

End Sub

And what does EndScreenSaver look like? It's a procedure defined in the module that takes no parameters. See Listing 37.7.

Public Sub EndScreenSaver()

CursorOn

End

End Sub

And that's all there is to it. There's just one more thing. Normally, a screen saver goes away even if you just move the mouse. Shouldn't you put something in the MouseMove event? Yes, and that is in Listing 37.8.

Private Sub Form_MouseMove(Button As Integer, Shift As Integer, _

   X As Single, Y As Single)

Static OldX As Integer

Static OldY As Integer

If (OldX > 0 And OldY > 0) And _       'If this isn't the first time and

   (Abs(X — OldX) > 3 Or Abs(Y — OldY) > 3) Then    'there is a big move

       EndScreenSaver

End If

OldX = X

OldY = Y

End Sub

You probably were expecting a routine as simple as the one for the MouseDown and KeyDown events. This one adds a bit of code to verify that the mouse has, in fact, moved more than a pixel or two. Why bother to do this? Several reasons.

First, and most intuitive, you don't necessarily want an accidental touch of the mouse to cause the screen saver to go away. Verifying that there has been a significant change since the last MouseMove event does this.

Second, for some reason, when a window first is opened, Windows triggers a MouseMove event on it. This is not normally a problem. But it would be here, of course. The screen saver would end as soon as it starts. Verifying that there has been an actual mouse position change before you kill the screen saver fixes the problem.

Finally, Windows does another weird thing (keeping a list?). If you set the wait time in the Display Properties dialog box on the Screen Saver tab to two minutes, for example, Windows waits for two minutes without interruption before executing your screen saver (see Figure 37.4). So far so good. But then, every two minutes after that time, Windows will trigger a MouseMove event on your screen saver. For no reason at all. But again, testing to be sure the mouse did move in the MouseMove event solves the problem.


Figure 37.4. The wait time in the Display Properties dialog box, on the Screen Saver tab.

Generating the Finished Product

That's it! You now have a working product. If you try running it, you might be sad to see that it does nothing. That's because, you will recall, it is looking for a command-line argument: a /c to change its settings or an /s to display the screen saver. To test it, choose Tools | Options. The Options dialog box appears. From here, choose the Advanced tab. In the Command Line Arguments field, type /s (see Figure 37.5).


Figure 37.5. Specifying a command-line argument for design-time execution.

This edit enables you to simulate passing a command-line argument when you run an application at design time. By using /s, you can view the actual screen saver. (Trying /c would show you only the little message box you created, saying that no setup is necessary for this screen saver.)

Now when you run the screen saver, you should see a black screen with lots of pretty (or not so pretty, as the case may be) boxes appearing about once a second before your eyes.

Now you are ready to create the final executable. Creating a screen saver executable is not that different from creating any other executable. First, save your work. (You were doing that all the way along, weren't you?) Then choose File | Make EXE File to display the Make EXE File dialog box (see Figure 37.6).


Figure 37.6. The Make EXE File dialog box.

If you click the Options button, you see the EXE Options dialog box, as shown in Figure 37.7.


Figure 37.7. The EXE Options dialog box.

Here, you can specify an icon (by choosing an icon already assigned to one of the forms), a version number, comments, company name, product name, and so on. Fill all this out as you like. There is one crucial thing you must do here, though. Set the application title to this:

SCRNSAVE: Boxes

This line lets Visual Basic know that this application is to be generated as a screen saver. Click OK in the EXE Options dialog box to return to the Make EXE File dialog box. The only thing left is to make sure that the file is generated with an SCR extension. Do this by explicitly entering the extension. When you are done, the file name edit control should contain this:

Boxes.SCR

That's it. Click OK, and your new screen saver is generated. After it finishes, copy the screen saver to the Windows directory and then right-click on your desktop. Choose Properties from the pop-up menu and then click the Screen Saver tab after the Display Properties dialog box appears. Pull down the Screen Saver drop-down list and scroll up to find Boxes. Choose it. Click Preview. You should see the Boxes screen saver at work.

Now set the wait time to one minute, click OK, and wait. Soon, boxes again fill your field of view. You've done it. Good work.

Adding a Configuration Dialog Box

Now you have a template for creating any screen saver you can imagine. What will you do first? A good place to start would be to add a Configuration dialog box that is displayed after the user clicks Settings from the Screen Saver tab in the Display Properties dialog box.

You probably can figure out how to do this without my help at all, right? All you need to do is create a new form—call it frmSetup. What sort of configuration makes sense for the Boxes screen saver? How about providing an option to add a beep every time a box comes up? We'll call this new feature BeepBox. Nothing like a little aural stimulation to go with those fetching boxes.

Figure 37.8 shows an example of frmSetup.


Figure 37.8 The layout of frmSetup, providing access to the nifty new BeepBox feature.

Very simple. The thing that's not so simple is this: where are you going to store this setting? A global variable won't work. Your program is going to end and then be called with an /s parameter a second time before you actually display the screen saver. You'll have to go to an INI file. Add these two lines to your module's Declaration section (Listing 37.9).

Declare Function WritePrivateProfileString Lib "kernel32" _

    Alias "WritePrivateProfileStringA" _

    (ByVal lpApplicationName As String, _

    ByVal lpKeyName As Any, ByVal lpString As String, _

    ByVal lpFileName As String) As Long

Declare Function GetPrivateProfileString Lib "kernel32" _

    Alias "GetPrivateProfileStringA" _

    (ByVal lpApplicationName As String, _

    ByVal lpKeyName As Any, ByVal lpDefault As String, _

    ByVal lpReturnedString As String, ByVal nSize As Long, _

    ByVal lpFileName As String) As Long

If you haven't used these functions before, here's a quick explanation of how they work. (If you have, feel free to skip the next few paragraphs.)

What's a Profile?

A profile is another name for an INI file. INI files are used by Windows and lots of Windows applications to store information from one run to the next—things like configuration settings, preferences, and screen layouts. Because this is so commonly done by so many applications, Windows has provided a standardized profile layout and an easy way to access profile data. The format looks like Listing 37.10.

[Graphics]

Color=5

XPos=100

YPos=300

[Text]

Title=The Man About Town

CursorPos=15

CursorRow=7

And so on. You are looking at the contents of a single profile that has a file name with the extension INI. Notice that the file is divided into one or more sections. Each section has one or more variable=value entries. The variables are called Keys and the values can be strings or numerics. All data in a profile is stored this way.

To access and change data in a profile, you use the two Win32 API functions declared in Listing 37.9. WritePrivateProfileString adds or changes information in the profile. The arguments you send to it follow (in order):

If the section or key doesn't exist, it is created in the profile.

GetPrivateProfileString is the counterpart read function, and its parameters follow:

Remember that strings always are passed by reference and that the use of ByVal in a declaration indicates that a C-style, null-terminated string is passed rather than a Visual Basic-style string.


WARNING

Be careful when using GetPrivateProfileString or any external function that expects a string variable to be passed by reference. You cannot pass a dynamic string variable that was just declared. You must either pass a fixed-length string or create a dynamic string and fill it with spaces long enough to contain the returned string plus one character for a C null end-string character. C doesn't know how to deal with Visual Basic's dynamic string structure, so be sure that you give it enough room to work with.

Adding the Configuration Code

So now that you have GetPrivateProfileString and SetPrivateProfileString at your disposal, what should the OK and Cancel button Click events in your Setup dialog look like? See Listings 37.11 and 37.12.

Private Sub cmdOK_Click()

Dim i As Long

Dim sValue As String

If optBeepBoxOn.VALUE = True Then

    sValue = "TRUE"

Else

    sValue = "FALSE"

End If

i = WritePrivateProfileString("Screen Saver.Boxes", _

    "BeepBox", sValue, "CONTROL.INI")

Unload Me

End Sub
Private Sub cmdCancel_Click()

Unload Me

End Sub

After the user clicks OK, the code checks to see which option button was clicked and sets sValue to TRUE or FALSE appropriately. Then a WritePrivateProfileString sets a key called BeepBox in a section of the CONTROL.INI file called Screen Saver.Boxes to sValue. Normally, it is a bad idea to use the system INI files like WIN.INI, SYSTEM.INI, and CONTROL.INI to keep application-specific information. It usually is best to keep that stuff in an application-specific INI file. But screen savers are an exception. Almost every screen saver has a section with the header Screen Saver.name in the CONTROL.INI. You could have made your own INI file, but this is a more standardized way of doing it.

And don't forget to add Listing 37.13 to the frmSetup Open event.

Private Sub Form_Load()

Dim sBeepBox As String

Dim i As Long

Dim iSize As Integer

sBeepBox = "        " ' 8 spaces

iSize = 8

i = GetPrivateProfileString("Screen Saver.Boxes", "BeepBox", _

    "TRUE", sBeepBox, iSize, "CONTROL.INI")

If UCase(Left(sBeepBox, 4)) = "TRUE" Then

    optBeepBoxOn.VALUE = True

Else

    optBeepBoxOff.VALUE = True

End If

End Sub

The inverse function, GetPrivateProfileString, is called to get the value, and the option boxes are set appropriately. Notice that I put eight spaces into sBeepBox before passing it. Be sure to always give your strings more room than they need when they are passed by reference to an external function.

Integrating the BeepBox Feature

Okay, now you have a Configuration dialog box that enables the user to choose an option and remembers what the user chose. Now all that's left is to make the application use that information. First, declare a new global in the module's Declarations section:

Global giBeepBox As Integer

Add appropriate lines to your Main procedure so that it looks like Listing 37.14.

Sub Main()

Dim sBeepBox As String

Dim i As Integer

Dim iSize As Integer

If App.PrevInstance Then End

sBeepBox = "        " ' 8 spaces

iSize = 8

If InStr(Command, "/s") > 0 Then

    CursorOff

    i = GetPrivateProfileString("Screen Saver.Boxes", "BeepBox", _

        "TRUE", sBeepBox, iSize, "CONTROL.INI")

    If UCase(Left(sBeepBox, 4)) = "TRUE" Then

        giBeepBox = True

    Else

        giBeepBox = False

    End If

    frmScreenSaver.Show

ElseIf InStr(Command, "/c") > 0 Then

    frmSetup.Show

End If

End Sub

If you are to run the screen saver, you get the BeepBox value from the profile and set the giBeepBox flag. Now in frmScreenSaver, Timer1's Timer event should look like Listing 37.15.

Private Sub Timer1_Timer()

Dim X1, Y1, X2, Y2 As Integer

X1 = Rnd * (ScaleWidth — 1) + 1

Y1 = Rnd * (ScaleHeight — 1) + 1

X2 = Rnd * (ScaleWidth — 1) + 1

Y2 = Rnd * (ScaleHeight — 1) + 1

ForeColor = RGB(Rnd * 255 + 1, Rnd * 255 + 1, Rnd * 255 + 1)

Line (X1, Y1)—(X2, Y2), , BF

If giBeepBox = True Then Beep

End Sub

The only change is the last line, which beeps for the new box, if it is supposed to.

That's it. Make a new SCR file, move it to the Windows directory, and give it a try! Depending on the WAV you have set as your system beep, you will find the new BeepBox feature to be somewhere on a scale between mildly annoying to completely maddening. Good thing there's an option to turn it off!

Future Enhancements

Screen savers are easy to create in Visual Basic, once you get the structure down. You can use this code as a starting point to move on to bigger and better things. Here are some suggestions for future enhancements:

But most important, have fun!

Summary

In this chapter, you have walked through all the steps necessary to create a basic screen saver. It's not difficult, but there are some quirks. Now you have the tools you need to let your creativity run wild. Enjoy!

Previous Page TOC Next Page